﻿/* 
 * Copyright (C) 2014 Mehdi El Gueddari
 * http://mehdi.me
 *
 * This software may be modified and distributed under the terms
 * of the MIT license.  See the LICENSE file for details.
 */

#if !NET40
using System.Runtime.ExceptionServices;
#endif
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
#if EF6
using System.Data.Entity;
#endif
using EntityFramework.DbContextScope.Interfaces;
#if EFCore
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
#elif EF6

#endif

namespace EntityFramework.DbContextScope
{
    /// <summary>
    /// As its name suggests, DbContextCollection maintains a collection of DbContext instances.
    /// 
    /// What it does in a nutshell:
    /// - Lazily instantiates DbContext instances when its Get Of TDbContext () method is called
    /// (and optionally starts an explicit database transaction).
    /// - Keeps track of the DbContext instances it created so that it can return the existing
    /// instance when asked for a DbContext of a specific type.
    /// - Takes care of committing / rolling back changes and transactions on all the DbContext
    /// instances it created when its Commit() or Rollback() method is called.
    /// 
    ///顾名思义，DbContext collection维护一个DbContext实例的集合。
    ///
    ///简而言之：
    ///-在调用DbContext（）方法的Get Of TDbContext（）时惰性地实例化DbContext实例
    ///（并且可以选择启动显式数据库事务）。
    ///-跟踪它创建的DbContext实例，以便它可以返回现有的
    ///当请求特定类型的DbContext时。
    ///-负责提交/回滚所有DbContext上的更改和事务
    ///调用其Commit（）或Rollback（）方法时创建的实例。
    /// </summary>
    public class DbContextCollection : IDbContextCollection
    {
        private readonly Dictionary<Type, IDbContext> _initializedDbContexts;
#if EFCore
        private readonly Dictionary<IDbContext, IDbContextTransaction> _transactions; 
#elif EF6
        private readonly Dictionary<IDbContext, DbContextTransaction> _transactions;
#endif
        private IsolationLevel? _isolationLevel;
        private readonly IDbContextFactory _dbContextFactory;
        private bool _disposed;
        private bool _completed;
        private readonly bool _readOnly;

        internal Dictionary<Type, IDbContext> InitializedDbContexts => _initializedDbContexts;

        public DbContextCollection(bool readOnly = false, IsolationLevel? isolationLevel = null, IDbContextFactory dbContextFactory = null)
        {
            _disposed = false;
            _completed = false;

            _initializedDbContexts = new Dictionary<Type, IDbContext>();
#if EFCore
            _transactions = new Dictionary<IDbContext, IDbContextTransaction>();
#elif EF6
            _transactions = new Dictionary<IDbContext, DbContextTransaction>();
#endif


            _readOnly = readOnly;
            _isolationLevel = isolationLevel;
            _dbContextFactory = dbContextFactory;
        }

        public TDbContext Get<TDbContext>() where TDbContext : class, IDbContext
        {
            if (_disposed)
                throw new ObjectDisposedException("DbContextCollection");

            Type requestedType = typeof(TDbContext);

            if (!_initializedDbContexts.ContainsKey(requestedType))
            {
                // First time we've been asked for this particular DbContext type.
                // Create one, cache it and start its database transaction if needed.
                //我们第一次被要求使用这种特定的DbContext类型。
                //创建一个，缓存它并在需要时启动它的数据库事务。
                TDbContext dbContext = _dbContextFactory != null
                    ? _dbContextFactory.CreateDbContext<TDbContext>(_readOnly)
                    : Activator.CreateInstance<TDbContext>();

                _initializedDbContexts.Add(requestedType, dbContext);

#if EFCore
                dbContext.ChangeTracker.AutoDetectChangesEnabled = !_readOnly;
#elif EF6
                dbContext.Configuration.AutoDetectChangesEnabled = !_readOnly;
#endif

                if (_isolationLevel.HasValue)
                {
#if EFCore
                    IDbContextTransaction tran = dbContext.Database.BeginTransaction(_isolationLevel.Value);
#elif EF6
                    DbContextTransaction tran = dbContext.Database.BeginTransaction(_isolationLevel.Value);
#endif
                    _transactions.Add(dbContext, tran);
                }
            }

            return _initializedDbContexts[requestedType] as TDbContext;
        }

        public int Commit()
        {
            if (_disposed)
                throw new ObjectDisposedException("DbContextCollection");
            if (_completed)
                throw new InvalidOperationException("You can't call Commit() or Rollback() more than once on a DbContextCollection. All the changes in the DbContext instances managed by this collection have already been saved or rollback and all database transactions have been completed and closed. If you wish to make more data changes, create a new DbContextCollection and make your changes there.");

            // Best effort. You'll note that we're not actually implementing an atomic commit 
            // here. It entirely possible that one DbContext instance will be committed successfully
            // and another will fail. Implementing an atomic commit would require us to wrap
            // all of this in a TransactionScope. The problem with TransactionScope is that 
            // the database transaction it creates may be automatically promoted to a 
            // distributed transaction if our DbContext instances happen to be using different 
            // databases. And that would require the DTC service (Distributed Transaction Coordinator)
            // to be enabled on all of our live and dev servers as well as on all of our dev workstations.
            // Otherwise the whole thing would blow up at runtime. 

            // In practice, if our services are implemented following a reasonably DDD approach,
            // a business transaction (i.e. a service method) should only modify entities in a single
            // DbContext. So we should never find ourselves in a situation where two DbContext instances
            // contain uncommitted changes here. We should therefore never be in a situation where the below
            // would result in a partial commit. 

            //尽最大努力。你会注意到我们实际上并没有实现原子提交
            //给你。完全有可能成功提交一个DbContext实例
            //另一个会失败。实现原子提交需要我们包装
            //所有这些都在事务处理范围内。TransactionScope的问题是
            //它创建的数据库事务可能会自动升级为
            //如果我们的DbContext实例碰巧使用不同的
            //数据库。这将需要DTC服务（分布式事务协调器）
            //在我们所有的实时和开发服务器以及所有的开发工作站上启用。
            //否则整件事会在运行时爆炸。

            //实际上，如果我们的服务是按照合理的DDD方法实施的，
            //业务事务（即服务方法）只应修改单个实体中的实体
            //数据库上下文。因此，我们永远不应该在两个DbContext实例的情况下
            //此处包含未提交的更改。因此，我们决不应处于以下情况
            //会导致部分提交

#if !NET40
            ExceptionDispatchInfo lastError = null;
#else
            Exception lastError = null;
#endif
            int c = 0;

            foreach (IDbContext dbContext in _initializedDbContexts.Values)
            {
                try
                {
                    if (!_readOnly)
                    {
                        c += dbContext.SaveChanges();
                    }

                    // If we've started an explicit database transaction, time to commit it now.
#if EFCore
                    IDbContextTransaction tran = GetValueOrDefault(_transactions, dbContext);
#elif EF6
                    DbContextTransaction tran = GetValueOrDefault(_transactions, dbContext);
#endif
                    if (tran != null)
                    {
                        tran.Commit();
                        tran.Dispose();
                    }
                }
                catch (Exception e)
                {
#if !NET40
                    lastError = ExceptionDispatchInfo.Capture(e);
#else
                    lastError = e;
#endif
                }
            }

            _transactions.Clear();
            _completed = true;

#if !NET40
            lastError?.Throw(); // Re-throw while maintaining the exception's original stack track
#else
            if (lastError != null)
            {
                throw new Exception("Error occurred committing transaction.", lastError);
            }
#endif
            return c;
        }

#if !NET40
        public Task<int> CommitAsync()
        {
            return CommitAsync(CancellationToken.None);
        }

        public async Task<int> CommitAsync(CancellationToken cancelToken)
        {
            if (cancelToken == null)
                throw new ArgumentNullException(nameof(cancelToken));
            if (_disposed)
                throw new ObjectDisposedException("DbContextCollection");
            if (_completed)
                throw new InvalidOperationException("You can't call Commit() or Rollback() more than once on a DbContextCollection. All the changes in the DbContext instances managed by this collection have already been saved or rollback and all database transactions have been completed and closed. If you wish to make more data changes, create a new DbContextCollection and make your changes there.");

            // See comments in the sync version of this method for more details.

            ExceptionDispatchInfo lastError = null;

            int c = 0;

            foreach (IDbContext dbContext in _initializedDbContexts.Values)
            {
                try
                {
                    if (!_readOnly)
                    {
                        c += await dbContext.SaveChangesAsync(cancelToken).ConfigureAwait(false);
                    }

                    // If we've started an explicit database transaction, time to commit it now.
#if EFCore
                    IDbContextTransaction tran = GetValueOrDefault(_transactions, dbContext);
#elif EF6
                    DbContextTransaction tran = GetValueOrDefault(_transactions, dbContext);
#endif
                    if (tran != null)
                    {
                        tran.Commit();
                        tran.Dispose();
                    }
                }
                catch (Exception e)
                {
                    lastError = ExceptionDispatchInfo.Capture(e);
                }
            }

            _transactions.Clear();
            _completed = true;

            lastError?.Throw(); // Re-throw while maintaining the exception's original stack track

            return c;
        }
#endif
        public void Rollback()
        {
            if (_disposed)
                throw new ObjectDisposedException("DbContextCollection");
            if (_completed)
                throw new InvalidOperationException("You can't call Commit() or Rollback() more than once on a DbContextCollection. All the changes in the DbContext instances managed by this collection have already been saved or rollback and all database transactions have been completed and closed. If you wish to make more data changes, create a new DbContextCollection and make your changes there.");

#if !NET40
            ExceptionDispatchInfo lastError = null;
#else
            Exception lastError = null;
#endif

            foreach (IDbContext dbContext in _initializedDbContexts.Values)
            {
                // There's no need to explicitly rollback changes in a DbContext as
                // DbContext doesn't save any changes until its SaveChanges() method is called.
                // So "rolling back" for a DbContext simply means not calling its SaveChanges()
                // method. 

                // But if we've started an explicit database transaction, then we must roll it back.

                //不需要显式回滚DbContext中的更改
                //在调用其SaveChanges（）方法之前，DbContext不会保存任何更改。
                //因此，DbContext的“回滚”仅仅意味着不调用其SaveChanges（）
                //方法。

                //但是如果我们已经启动了显式数据库事务，那么我们必须回滚它。
#if EFCore
                    IDbContextTransaction tran = GetValueOrDefault(_transactions, dbContext);
#elif EF6
                DbContextTransaction tran = GetValueOrDefault(_transactions, dbContext);
#endif
                if (tran != null)
                {
                    try
                    {
                        tran.Rollback();
                        tran.Dispose();
                    }
                    catch (Exception e)
                    {
#if !NET40
                        lastError = ExceptionDispatchInfo.Capture(e);
#else
                        lastError = e;
#endif
                    }
                }
            }

            _transactions.Clear();
            _completed = true;

#if !NET40
            lastError?.Throw(); // Re-throw while maintaining the exception's original stack track
#else
            if (lastError != null)
            {
                throw new Exception("Error occurred committing transaction.", lastError);
            }
#endif      
        }

        public void Dispose()
        {
            if (_disposed)
                return;

            // Do our best here to dispose as much as we can even if we get errors along the way.
            // Now is not the time to throw. Correctly implemented applications will have called
            // either Commit() or Rollback() first and would have got the error there.

            //在这里尽我们最大的努力处理尽可能多的问题，即使我们在处理过程中遇到了错误。
            //现在不是投掷的时候。正确实现的应用程序将调用
            //或者Commit（）或者Rollback（）第一个，就会得到错误。
            if (!_completed)
            {
                try
                {
                    if (_readOnly) Commit();
                    else Rollback();
                }
                catch (Exception e)
                {
                    System.Diagnostics.Debug.WriteLine(e);
                }
            }

            foreach (IDbContext dbContext in _initializedDbContexts.Values)
            {
                try
                {
                    dbContext.Dispose();
                }
                catch (Exception e)
                {
                    System.Diagnostics.Debug.WriteLine(e);
                }
            }

            _initializedDbContexts.Clear();
            _disposed = true;
        }

        /// <summary>
        /// Returns the value associated with the specified key or the default 
        /// value for the TValue  type.
        /// </summary>
        private static TValue GetValueOrDefault<TKey, TValue>(IDictionary<TKey, TValue> dictionary, TKey key)
        {
            TValue value;
            return dictionary.TryGetValue(key, out value) ? value : default(TValue);
        }
    }
}